package network_programming.np2_nonblocking_communication;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;

/**
 * 非阻塞模式 EchoServer
 * 只需启动一个主线程，就能同时处理3件事
 *      1、接收客户的连接
 *      2、接收客户发送的数据
 *      3、向客户发回响应数据
 *
 * EchoServer 委托 Selector 来负责监控接收连接就绪事件、读就绪事件和写就绪事件，如有特定事件发生就处理该事件
 *
 * */
public class EchoServerNonblocking {
    private int port = 9099;
    private Selector selector;
    private ServerSocketChannel serverSocketChannel;
//    private Charset charset = Charset.forName("GBK");
    private Charset charset = StandardCharsets.UTF_8;

    public EchoServerNonblocking() throws IOException {
        selector = Selector.open();     // 创建一个Selector对象
        serverSocketChannel = ServerSocketChannel.open();   // 创建一个ServerSocketChannel对象

        // 设置为在同一个主机上关闭服务器程序后，紧接着再次启动该服务器程序时，可以顺利绑定相同端口
        serverSocketChannel.socket().setReuseAddress(true);

        // 使ServerSocketChannel工作于非阻塞模式
        serverSocketChannel.configureBlocking(false);   // 配置工作模式（阻塞或非阻塞），默认为tru，即阻塞

        // 将服务器进程与一个本地端口绑定
        serverSocketChannel.socket().bind(new InetSocketAddress(port));

        System.out.println("服务器启动成功");
    }

    public void service() throws IOException {
        // 向Selector注册连接就绪事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); // 用给定的选择器注册此通道，返回选择键
        while (selector.select() > 0) {     // 轮询
                // selector.select()返回相关事件已经发生的SelectionKey对象的数目，如果未发生任何事件则该方法会阻塞
            Set<SelectionKey> readyKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = readyKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();     // 取出一个SelectionKey
                iterator.remove();      // 将key冲Selector的selected-key集合中删除
                try {
                    if (key.isAcceptable()) {
                        // 处理接收连接就绪事件
                    	
                    	// 获取与SelectionKey关联的ServerSocketChannel
                    	ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                    	SocketChannel socketChannel = ssc.accept();	// 获取与客户连接的SocketChannel
                    	System.out.println("接收到客户连接，来自：" + socketChannel.socket().getInetAddress()
                    			+ ":" + socketChannel.socket().getPort());
                    	socketChannel.configureBlocking(false);		// 把SocketChannel设置为非阻塞模式
                    	ByteBuffer buffer = ByteBuffer.allocate(1024);	// 创建存放用户发送来的数据缓冲区
                    	
                    	// SocketChannel想Selector注册读就绪事件和写就绪事件，并关联一个buffer附件
                    	socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE, buffer);
                    }
                    if (key.isReadable()) {
                        // 处理读就绪事件
                    	receive(key);
                    	
                    }
                    if (key.isWritable()) {
                        // 处理写就绪事件
                    	send(key);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                    if (key != null)
                        try {
                            // 使该SelectionKey失效
                            // 使得Selector不再监控这个SelectionKey感兴趣的事件
                            key.cancel();
                            key.channel().close();  // 关闭与这个SelectionKey关联的SocketChannel
                        } catch (Exception ex) {ex.printStackTrace();}
                }
            }
        }
    }
    
    /**
     * 处理读就绪事件
     * @param key	SelectionKey对象
     * @throws IOException 
     * */
    public void receive(SelectionKey key) throws IOException {
    	ByteBuffer buffer = (ByteBuffer) key.attachment();	// 获取与SelectionKey关联的附件
    	SocketChannel socketChannel = (SocketChannel) key.channel();	// 获取与SelectionKey关联的SocketChannel
    	ByteBuffer readBuf = ByteBuffer.allocate(32);	// 存放读到的数据
    	socketChannel.read(readBuf);
    	readBuf.flip();		// 把极限设置为位置，把位置设置为0
    	
    	buffer.limit(buffer.capacity());	// 把buffer的极限设为容量
    	// 把readBuf中的内容拷贝到buffer中，假定buffer的容量足够大，不会出现缓冲区溢出异常
    	buffer.put(readBuf);
    }
    
    /**
     * 处理写就绪事件
     * @param key	SelectionKey对象
     * @throws IOException 
     * */
    public void send(SelectionKey key) throws IOException {
    	ByteBuffer buffer = (ByteBuffer) key.attachment();	// 获取与SelectionKey关联的附件
    	SocketChannel socketChannel = (SocketChannel) key.channel();	// 获取与SelectionKey关联的SocketChannel
    	buffer.flip();	// 把极限设置为位置，把位置设置为0
    	String data = decode(buffer);	// 按GBK编码把buffer总的字节转换为字符串
    	if (data.indexOf("\r\n") == -1)		// 未读到一行数据则返回
    		return;
    	String outputData = data.substring(0, data.indexOf("\n") + 1);
    	System.out.println(outputData);
    	ByteBuffer outputBuffer = encode("echo: " + outputData);	// 按GBK编码把输出字符串转换为字节
    	// 在非阻塞模式下SocketChannel.write(outputBuffer)方法并不能保证一次就把outputBuffer中的所有字节都发送完，
    	// 奉行能发送多少就发送多少的原则，需用循环来保证全部发送
    	while (outputBuffer.hasRemaining())		// 判断outputBuffer是否还有剩余
    		socketChannel.write(outputBuffer);
    	
    	ByteBuffer temp = encode(outputData);
    	buffer.position(temp.limit());	// 把buffer的位置设为temp的极限
    	buffer.compact();		// 删除缓冲区内从0到当前位置position的内容，即删除已处理的数据
    	
    	// 如果已经出现了字符串“bye\r\n”，就使SelectionKey失效，并关闭SocketCannel
    	if ("bye\r\n".equals(outputData)) {
    		key.cancel();
    		socketChannel.close();
    		System.out.println("关闭与客户的连接");
    	}
    }

    /**
     * 编码
     * */
    private ByteBuffer encode(String str) {
		return charset.encode(str);
	}

	/**
     * 解码
     * */
	private String decode(ByteBuffer buffer) {
		CharBuffer charBuffer = charset.decode(buffer);
		return charBuffer.toString();
	}
	
	public static void main(String args[]) throws IOException {
		EchoServerNonblocking server = new EchoServerNonblocking();
		server.service();
	}
}
